/*
  Java base64-decoding FilterOutputStream
  Copyright (c) 1999 by Michael N. Lipp
 
  This program is free software.
 
  You may redistribute it and/or modify it under the terms of the GNU
  General Public License as published by the Free Software Foundation.
  Version 2 of the license should be included with this distribution in
  the file COPYING.GPL. If the license is not included	with this 
  distribution, you may find a copy at the FSF web site 
  at 'www.gnu.org' or 'www.fsf.org', or you may write to the
  Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139 USA.

  THIS SOFTWARE IS PROVIDED AS-IS WITHOUT WARRANTY OF ANY KIND,
  NOT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY. THE AUTHOR
  OF THIS SOFTWARE, ASSUMES _NO_ RESPONSIBILITY FOR ANY
  CONSEQUENCE RESULTING FROM THE USE, MODIFICATION, OR
  REDISTRIBUTION OF THIS SOFTWARE. 
 
*/
package gnu.inet.mime.base64;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;

/** writes bas64 encoded data to a raw stream.
 * Note that this implementation does not attempt to do any canonalization.
 * I.e. if used to encode text in an RFC1521 conformant way, care must be 
 * taken to convert line breaks to CRLF before passing the data to the
 * <code>Base64OutputStream</code>. The lack of canonalization is considered
 * a feature as it allows the <code>Base64OutputStream</code> to be used for
 * binary data. (Maybe one day I'll add a TextCanonalizationOutputStream)
 *
 * @author Michael N.Lipp
 */
public class Base64OutputStream extends FilterOutputStream
{

	private final static char charCodes[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
			'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
			'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3',
			'4', '5', '6', '7', '8', '9', '+', '/' };

	private byte encBuf[] = new byte[4];
	private int encoded;
	private int surplus;
	private int col;
	private boolean finishOnFlush;

	/**
	 * The constructor for the encoding output stream.
	 *
	 * @param out the underlying output stream.
	 * @param finishOnFlush controls the behaviour of the flush method.
	 * @see org.tsx.mnl.io.base64.Base64OutputStream#flush
	 */
	public Base64OutputStream(OutputStream out, boolean finishOnFlush)
	{
		super(out);
		encoded = 0;
		col = 0;
		this.finishOnFlush = finishOnFlush;
	}

	/**
	 * Sets the value of finishOnFlush.
	 *
	 * @param value the new value.
	 * @see org.tsx.mnl.io.base64.Base64OutputStream#flush
	 */
	public void setFinishOnFlush(boolean value)
	{
		finishOnFlush = value;
	}

	/**
	 * Encodes and writes a single byte to the output stream.
	 *
	 * @param b the byte to be written.
	 */
	public void write(int b) throws IOException
	{
		byte a[] = { (byte)(b & 0xff) };
		write(a, 0, 1);
	}

	/**
	 * Encodes and writes len bytes from array data to the outputstream, 
	 * starting with the byte at position off.
	 *
	 * @param data the array holding the bytes to be written.
	 * @param off the position off the first byte to be written within the 
	 * array.
	 * @param len the number of bytes to be written.
	 */
	public void write(byte[] data, int off, int len) throws IOException
	{
		int pos = off;
		// fill encBuf if possible
		while (encoded > 0 && pos < off + len)
		{
			encodeByte(data[pos]);
			pos += 1;
		}
		// encode triples for efficiency
		while (pos + 2 < off + len)
		{
			byte b0 = data[pos];
			byte b1 = data[pos + 1];
			byte b2 = data[pos + 2];
			encBuf[0] = (byte)charCodes[((b0 >> 2) & 0x3f)];
			encBuf[1] = (byte)charCodes[((b0 & 0x3) << 4) | ((b1 >> 4) & 0xf)];
			encBuf[2] = (byte)charCodes[((b1 & 0xf) << 2) | ((b2 >> 6) & 0x3)];
			encBuf[3] = (byte)charCodes[b2 & 0x3f];
			out.write(encBuf);
			encoded = 0;
			col += 4;
			if (col >= 76)
			{
				out.write('\n');
				col = 0;
			}
			pos += 3;
		}
		// encode rest
		while (pos < off + len)
		{
			encodeByte(data[pos]);
			pos += 1;
		}
	}

	private void encodeByte(byte b) throws IOException
	{
		switch (encoded)
		{
			case 0:
				encBuf[0] = (byte)charCodes[(b >> 2) & 0x3f];
				surplus = ((b & 0x3) << 4);
				encBuf[1] = (byte)charCodes[surplus];
				encoded += 1;
				break;
			case 1:
				encBuf[1] = (byte)charCodes[surplus | ((b >> 4) & 0xf)];
				surplus = ((b & 0xf) << 2);
				encBuf[2] = (byte)charCodes[surplus];
				encoded += 1;
				break;
			case 2:
				encBuf[2] = (byte)charCodes[surplus | ((b >>> 6) & 0x3)];
				encBuf[3] = (byte)charCodes[b & 0x3f];
				out.write(encBuf);
				encoded = 0;
				col += 4;
				if (col >= 76)
				{
					out.write('\n');
					col = 0;
				}
				break;
		}
	}

	/**
	 * This method is to write all buffered data to its destination.
	 * With a base64 encoding there arises a problem: if an encoding character
	 * isn't completly known yet because its value depends partially
	 * on the last byte written and partially on the next byte to be
	 * written, then flush can't write it without assuming that there
	 * will be no more bytes coming. 
	 * 
	 * The behaviour for flush can therefore
	 * be controlled by the flag finishOnFlush when the stream is created. 
	 * If the flag is set, flush will assume that no more data is to be
	 * written after flush has been called, thus effectivly finishing the
	 * stream of encoded data. This behaviour is especially useful, when
	 * the Base64OutputStream is created with an underlying output stream 
	 * that contains mixed
	 * content. In this case, we cannot call close on the Base64OutputStream
	 * because this would make the underlying OutputStream unusable as well.
	 *
	 * @see org.tsx.mnl.io.base64.Base64OutputStream
	 * @see java.io.OutputStream#flush
	 */
	public void flush() throws IOException
	{
		if (finishOnFlush)
			doFlush();
	}

	private void doFlush() throws IOException
	{
		switch (encoded)
		{
			case 1:
				encBuf[2] = (byte)'=';
			case 2:
				encBuf[3] = (byte)'=';
				out.write(encBuf);
				col += 4;
				break;
		}
		if (col > 0)
		{
			out.write('\n');
			col = 0;
		}
	}

	/**
	 * This method finishes the encoded stream and calls close on the
	 * underlying output stream.
	 */
	public void close() throws IOException
	{
		doFlush();
		out.close();
	}
}
